﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Globalization;

namespace SecureCookie.Implementation.Protocol
{
    /// <summary>
    /// Implementation for the "Secure Cookie Protocol" described in the attached PDF file "Protocol description.pdf" (see solution items)
    /// </summary>
    internal static class SecureCookieProtocol
    {
        /// <summary>
        /// Secures a cookie using the "Secure Cookie Protocol". To secure the cookie a validation hash code and expiration is added to the cookie.
        /// </summary>
        /// <param name="cookieToSecure">The cookie to secure</param>
        /// <param name="serverSideData">Data only known and created by the server</param>
        /// <param name="requestState">TODO</param>
        /// <returns>A secured version of the cookie</returns>
        public static HttpCookie MakeSecure(HttpCookie cookieToSecure, ServerSideData serverSideData, IRequestState requestState)
        {
            var securedCookie = new HttpCookie(cookieToSecure.Name);

            var expirationDateTime = cookieToSecure.Expires;
            if (expirationDateTime == DateTime.MaxValue || expirationDateTime == DateTime.MinValue)
            {
                if (SecureCookieSettings.Default.MaxLifetimeForSessionBasedCookiesIsSliding || !requestState.HasExpirationFromIncomingCookie(cookieToSecure.Name))
                    expirationDateTime = DateTime.Now.Add(serverSideData.MaxLifetimeForSessionBasedCookies);
                else
                    expirationDateTime = requestState.ExpirationFromIncomingCookie(cookieToSecure.Name);
            }

            var hash =
                GetExternalHash(cookieToSecure.Name,
                    expirationDateTime,
                    cookieToSecure.Value,
                    serverSideData.ClientUniqueValue,
                    GetInternalHash(cookieToSecure.Name, expirationDateTime, serverSideData.SecretKey));

            securedCookie.SetHashPart(hash);
            securedCookie.SetExpirationPart(expirationDateTime);
            securedCookie.SetDataPart(cookieToSecure.Value);
            securedCookie.Expires = cookieToSecure.Expires;

            return securedCookie;
        }

        /// <summary>
        /// Validates a cookie that is send from a client and returns the validation result.
        /// </summary>
        /// <param name="cookieToValidate">The (probably secured) cookie to validate</param>
        /// <param name="serverSideData">The data created by and only known on the server</param>
        /// <param name="requestState">TODO</param>
        /// <returns>The result of the validation</returns>
        public static ValidationResult Validate(HttpCookie cookieToValidate, ServerSideData serverSideData, IRequestState requestState)
        {
            if (cookieToValidate.Values == null || cookieToValidate.Values.Count != 3 || !cookieToValidate.Values.HasKeys())
                return ValidationResult.InvalidIsNotSecured;

            var expirationDate = cookieToValidate.GetExpirationPart();
            if (expirationDate == null
                || expirationDate.Value == DateTime.MinValue
                || expirationDate.Value == DateTime.MaxValue)
                return ValidationResult.InvalidUnexpectedValues;
            if (expirationDate.Value < DateTime.Now)
                return ValidationResult.InvalidIsExpired;

            var internalHash = GetInternalHash(cookieToValidate.Name, expirationDate.Value, serverSideData.SecretKey);
            var expectedExternalHash = GetExternalHash(cookieToValidate.Name, expirationDate.Value, cookieToValidate.GetDataPart(), serverSideData.ClientUniqueValue, internalHash);

            var actualHash = cookieToValidate.GetHashPart();
            if (string.IsNullOrEmpty(actualHash))
                return ValidationResult.InvalidUnexpectedValues;
            if (string.Equals(actualHash, expectedExternalHash, StringComparison.Ordinal))
            {
                requestState.RegisterExpirationFromIncomingCookie(expirationDate.Value, cookieToValidate.Name);
                return ValidationResult.Valid;
            }
            else
                return ValidationResult.InvalidHashingCode;
        }

        /// <summary>
        /// Returns the public hash that is created by the internal private hash and the contents of the cookie.
        /// </summary>
        /// <param name="cookieName">The name of the cookie that needs to be secured</param>
        /// <param name="expirationDateTime">The absolute expiration datetime of the cookie that needs to be secured</param>
        /// <param name="data">The data that needs to be secured</param>
        /// <param name="clientUniqueValue">A unique value to identify the client</param>
        /// <param name="internalHash">The internal private hash</param>
        /// <returns>The public hash that can be used in the secured cookie</returns>
        private static string GetExternalHash(string cookieName, DateTime expirationDateTime, string data, string clientUniqueValue, string internalHash)
        {
            return Hash(string.Format(CultureInfo.InvariantCulture, "{0}|{1}|{2}|{3}", cookieName, expirationDateTime.Ticks, data, clientUniqueValue), internalHash);
        }
        /// <summary>
        /// Returns the private hash that is created by using the expiration date/cookie name and the secret key from the server.
        /// </summary>
        /// <param name="cookieName">The name of the cookie that needs to be secured</param>
        /// <param name="expirationDateTime">The absolute expiration datetime of the cookie that needs to be secured</param>
        /// <param name="secretKey">The secred key, only known serverside</param>
        /// <returns>A hash created by the cookieName,expirationDateTime and secretKey combination</returns>
        private static string GetInternalHash(string cookieName, DateTime expirationDateTime, string secretKey)
        {
            return Hash(string.Format(CultureInfo.InvariantCulture, "{0}|{1}", cookieName, expirationDateTime.Ticks), secretKey);
        }
        /// <summary>
        /// Returns a base64 encoded hash for the given data with the given key.
        /// </summary>
        /// <param name="data">The data from which a hash should be created</param>
        /// <param name="key">The key to use for the hash</param>
        /// <returns>A base64 encoded hash generated for the given data/key combination</returns>
        private static string Hash(string data, string key)
        {
            using (var hash = new System.Security.Cryptography.HMACMD5(Encoding.UTF8.GetBytes(key)))
            {
                return Convert.ToBase64String(hash.ComputeHash(Encoding.UTF8.GetBytes(data)));
            }
        }
        /// <summary>
        /// Creates a <see cref="HttpCookie"/> with only the original value from a secured <see cref="HttpCookie"/>
        /// </summary>
        /// <param name="cookie">A secured <see cref="HttpCookie"/></param>
        /// <returns>A <see cref="HttpCookie"/> with only the original value from a secured <see cref="HttpCookie"/></returns>
        internal static HttpCookie Restore(HttpCookie cookie)
        {
            return new HttpCookie(cookie.Name, cookie.GetDataPart());
        }
    }
}
